package edu.the.last.graphic;

import java.awt.BorderLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.List;

import javax.help.CSH;
import javax.help.HelpBroker;
import javax.help.HelpSet;
import javax.help.HelpSetException;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.SoftBevelBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataListener;

import edu.the.last.Messages;
import edu.the.last.audio.Capture;
import edu.the.last.audio.PlayChord;
import edu.the.last.audio.Playback;
import edu.the.last.audio.Tick;
import edu.the.last.chord.factory.ChordFactory;
import edu.the.last.chord.factory.ChordInterface;
import edu.the.last.fft.Frequency;
import edu.the.last.help.AboutDialog;
import edu.the.last.modell.Music;

/**
 * 
 * @author albertcsani
 * 
 */
public class MainFrame extends JPanel implements ActionListener {
	private static final long serialVersionUID = 1L;
	private Music music = new Music(120, 4);
	private JButton playB, captB, recalcB;
	private JLabel infoLabel = new JLabel(""); //$NON-NLS-1$
	private JMenuBar menuBar;
	private JMenu menu;
	private JMenuItem menuItem;

	private JLabel strategyLabel = new JLabel(Messages.getString("FelLe.strategy")); //$NON-NLS-1$
	private JLabel bpmLabel = new JLabel(Messages.getString("FelLe.BPM")); //$NON-NLS-1$
	private JLabel bpbLabel = new JLabel(Messages.getString("FelLe.BPB")); //$NON-NLS-1$
	private JLabel volLabel = new JLabel(Messages.getString("FelLe.Vol")); //$NON-NLS-1$
	private JTextField bpmTF = new JTextField(Integer.toString(music.getBpm()), 3);
	private JTextField bpbTF = new JTextField(Integer.toString(music.getBpb()), 3);
	private JSlider volSlider = new JSlider(JSlider.HORIZONTAL, 0, 130, 50);
	private JComboBox strategy;
	private Tick tick = new Tick(music.getBpm(), music.getBpb());
	private PlayChord pc;
	private TimeCounter timeCounter = new TimeCounter(infoLabel);
	private double duration;

	private Frequency freq;
	private ChordInterface chordInterface;
	private Playback playback = new Playback(this, music);
	private SamplingGraph samplingGraph;
	private Capture capture = new Capture(music);

	private static HelpBroker mainHelpBroker = null;

	private static CSH.DisplayHelpFromSource csh = null;

	private static final String mainHelpSetName = "helpset/ChordFactory.hs";

	/**
	 * Default constructor for the class, it creates the GUI
	 * 
	 * @throws Exception
	 */
	public MainFrame() throws Exception {

		setLayout(new BorderLayout());
		EmptyBorder eb = new EmptyBorder(5, 5, 5, 5);
		SoftBevelBorder sbb = new SoftBevelBorder(SoftBevelBorder.LOWERED);
		setBorder(eb);

		JPanel p = new JPanel();
		p.setLayout(new BorderLayout());

		JPanel tfPanel = new JPanel();
		tfPanel.setBorder(new EmptyBorder(10, 0, 0, 0));

		JPanel p1 = new JPanel();
		p1.setLayout(new BoxLayout(p1, BoxLayout.X_AXIS));

		JPanel p2 = new JPanel();
		p2.setBorder(sbb);
		p2.setLayout(new BoxLayout(p2, BoxLayout.Y_AXIS));

		JPanel buttonsPanel = new JPanel();
		buttonsPanel.setBorder(new EmptyBorder(0, 0, 5, 0));

		// constructing the menu
		menuBar = new JMenuBar();
		menu = new JMenu(Messages.getString("FelLe.Menu.File")); //$NON-NLS-1$
		menu.setMnemonic(KeyEvent.VK_F);
		menuBar.add(menu);

		menuItem = new JMenuItem(Messages.getString("FelLe.Menu.Load")); //$NON-NLS-1$
		menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, ActionEvent.CTRL_MASK));
		menuItem.addActionListener(this);
		menu.add(menuItem);

		menuItem = new JMenuItem(Messages.getString("FelLe.Menu.Save")); //$NON-NLS-1$
		menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
		menuItem.addActionListener(this);
		menu.add(menuItem);

		menuItem = new JMenuItem(Messages.getString("FelLe.Menu.Exit")); //$NON-NLS-1$
		menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK));
		menuItem.addActionListener(this);
		menu.add(menuItem);

		// Help Menu, H - Mnemonic
		menu = new JMenu(Messages.getString("FelLe.Menu.Help")); //$NON-NLS-1$
		menu.setMnemonic(KeyEvent.VK_H);

		menuItem = new JMenuItem(Messages.getString("FelLe.Menu.Help")); //$NON-NLS-1$
		menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0));
		{
			if (mainHelpBroker == null) {
				HelpSet mainHelpSet = null;
				try {
					URL hsURL = HelpSet.findHelpSet(null, mainHelpSetName);
					if (hsURL == null)
						System.out.println("HelpSet " + mainHelpSetName + " not found.");
					else
						mainHelpSet = new HelpSet(null, hsURL);
				} catch (HelpSetException ee) {
					System.out.println("HelpSet " + mainHelpSetName + " could not be opened.");
					System.out.println(ee.getMessage());
				}
				if (mainHelpSet != null)
					mainHelpBroker = mainHelpSet.createHelpBroker();
				if (mainHelpBroker != null)
					csh = new CSH.DisplayHelpFromSource(mainHelpBroker);
				if (csh != null)
					menuItem.addActionListener(csh);
			}
		}

		menuItem.addActionListener(this);
		menu.add(menuItem);

		menu.addSeparator();

		menuItem = new JMenuItem(Messages.getString("FelLe.Menu.About")); //$NON-NLS-1$
		menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, ActionEvent.CTRL_MASK));
		menuItem.addActionListener(this);
		menu.add(menuItem);

		menuBar.add(Box.createHorizontalGlue());
		menuBar.add(menu);

		p.add(menuBar, BorderLayout.NORTH);
		// end of constructing the menu

		// filling up the combobox and creating a model for it
		List<ChordFactory> list = ChordFactory.getInstances();
		final ChordFactory lista[] = list.toArray(new ChordFactory[0]);
		strategy = new JComboBox(lista);
		strategy.setModel(new ComboBoxModel() {
			private ChordInterface selected = lista[0].getChords();

			@Override
			public void removeListDataListener(ListDataListener arg0) {
				// this should never occur
			}

			@Override
			public int getSize() {
				return lista.length;
			}

			@Override
			public Object getElementAt(int arg0) {
				return lista[arg0].getChords().toString();
			}

			@Override
			public void addListDataListener(ListDataListener arg0) {
				// this should never occur
			}

			@Override
			public void setSelectedItem(Object anItem) {
				for (int i = 0; i < lista.length; i++) {
					if (lista[i].getChords().toString().equals(anItem)) {
						selected = lista[i].getChords();
						chordInterface = selected;
					}
				}
			}

			@Override
			public Object getSelectedItem() {
				chordInterface = selected;
				return selected;
			}
		});
		tfPanel.add(strategyLabel);
		tfPanel.add(strategy);

		// creating the text fields so they accept only positive integer data.
		bpmLabel.setToolTipText(Messages.getString("FelLe.Label.BPM")); //$NON-NLS-1$
		bpmTF.setToolTipText(Messages.getString("FelLe.Tip.BPM")); //$NON-NLS-1$
		bpmTF.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				char c = e.getKeyChar();
				if (!((c >= '0') && (c <= '9') || (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
					getToolkit().beep();
					e.consume();
				}
			}
		});

		tfPanel.add(bpmLabel);
		tfPanel.add(bpmTF);

		bpbLabel.setToolTipText(Messages.getString("FelLe.Label.BPB")); //$NON-NLS-1$
		bpbTF.setToolTipText(Messages.getString("FelLe.Tip.BPB")); //$NON-NLS-1$
		bpbTF.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				char c = e.getKeyChar();
				if (!((c >= '0') && (c <= '9') || (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
					getToolkit().beep();
					e.consume();
				}
			}
		});
		tfPanel.add(bpbLabel);
		tfPanel.add(bpbTF);
		recalcB = addButton(Messages.getString("FelLe.Button.Refresh"), tfPanel, true); //$NON-NLS-1$

		p2.add(tfPanel);

		volLabel.setToolTipText(Messages.getString("FelLe.Label.ChVol")); //$NON-NLS-1$
		volSlider.setToolTipText(Messages.getString("FelLe.Tip.ChVol")); //$NON-NLS-1$
		// listener that can change the volume while playing
		volSlider.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				int vol = ((JSlider) e.getSource()).getValue();
				if (pc != null) {
					pc.setVolume(vol);
				}
			}
		});
		buttonsPanel.add(volLabel);
		buttonsPanel.add(volSlider);

		playB = addButton(Messages.getString("FelLe.Play"), buttonsPanel, false); //$NON-NLS-1$
		playB.setToolTipText(Messages.getString("FelLe.Tip.Play")); //$NON-NLS-1$
		captB = addButton(Messages.getString("FelLe.Button.Record"), buttonsPanel, true); //$NON-NLS-1$
		p2.add(buttonsPanel);

		// creating the panel for the wave form
		JPanel samplingPanel = new JPanel(new BorderLayout());
		eb = new EmptyBorder(10, 20, 20, 20);
		samplingPanel.setBorder(new CompoundBorder(eb, sbb));

		JScrollPane samplingScrollPane = new JScrollPane(samplingGraph = new SamplingGraph(music));
		samplingPanel.add(samplingScrollPane);
		p2.add(samplingPanel);

		// label for displaying the time
		p2.add(infoLabel);

		p1.add(p2);

		p.add(p1, BorderLayout.CENTER);
		add(p);
	}

	/**
	 * Enables the Record and the Refresh buttons ...
	 */
	public void enableButtons() {
		playB.setText(Messages.getString("FelLe.Play")); //$NON-NLS-1$
		captB.setEnabled(true);
		recalcB.setEnabled(true);
	}

	/**
	 * Just for saving some lines of code, and losing it whit this comments....
	 * It creates a JButton on a specific JPanel whit a specified text and state
	 * 
	 * @param name
	 *            the text on the button
	 * @param p
	 *            the panel on whereat the button will appear
	 * @param state
	 *            is enabled?
	 * @return a JButton on p whit
	 */
	private JButton addButton(String name, JPanel p, boolean state) {
		JButton b = new JButton(name);
		b.addActionListener(this);
		b.setEnabled(state);
		p.add(b);
		return b;
	}

	/**
	 * a listener for the buttons
	 */
	public void actionPerformed(ActionEvent e) {
		Object obj = e.getSource();
		if (obj.equals(playB)) {
			// start playing
			if (playB.getText().startsWith(Messages.getString("FelLe.Play"))) { //$NON-NLS-1$
				try {
					samplingGraph.initSamplingGraph();
				} catch (Exception e1) {
					e1.printStackTrace();
				}
				pc = new PlayChord(music, volSlider.getValue());
				if (e.getModifiers() != 17) {//just the recording (Ctrl)
					playback.start();
				}
				samplingGraph.start();
				if (e.getModifiers() != 18) { //just the chords (Shift)
					pc.start();
				}
				recalcB.setEnabled(false);
				captB.setEnabled(false);
				playB.setText(Messages.getString("FelLe.Stop")); //$NON-NLS-1$
				timeCounter = new TimeCounter(infoLabel);
				DecimalFormat df = new DecimalFormat(Messages.getString("FelLe.Format")); //$NON-NLS-1$
				timeCounter.setInfo(Messages.getString("FelLe.Playing"), Messages.getString("FelLe.Of") + df.format(duration)); //$NON-NLS-1$ //$NON-NLS-2$
				timeCounter.setLimit(duration);
				timeCounter.start();
			} else {
				// stop the playback
				playback.stop();
				samplingGraph.stop();
				pc.stopChords();
				recalcB.setEnabled(true);
				captB.setEnabled(true);
				playB.setText(Messages.getString("FelLe.Play")); //$NON-NLS-1$
				timeCounter.stopCount();
			}
		} else if (obj.equals(captB)) {
			// begin recording
			if (captB.getText().startsWith(Messages.getString("FelLe.Button.Record"))) { //$NON-NLS-1$
				music.clear();
				music.setBpb(Integer.parseInt(bpbTF.getText()));
				music.setBpm(Integer.parseInt(bpmTF.getText()));
				capture = new Capture(music);
				tick = new Tick(music.getBpm(), music.getBpb());
				tick.start();
				capture.start();

				timeCounter = new TimeCounter(infoLabel);
				timeCounter.setInfo(Messages.getString("FelLe.Recording"), ""); //$NON-NLS-1$ //$NON-NLS-2$
				timeCounter.start();
				playB.setEnabled(false);
				recalcB.setEnabled(false);
				captB.setText(Messages.getString("FelLe.Stop")); //$NON-NLS-1$
			} else {
				// end recording
				capture.stop();
				tick.stopTick();
				timeCounter.stopCount();
				// waiting for the Capture thread to create the audioImputStream
				while (music.getAudioBytes() == null) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
				}
				long milliseconds = (long) ((music.getAudioBytes().length * 1000) / music.getFormat().getFrameRate());
				duration = (milliseconds / 1000.0);
				infoLabel.setText(Messages.getString("FelLe.RecLen") + duration); //$NON-NLS-1$
				try {
					// getting the pitch values of the recorded audio using fft
					freq = new Frequency(music);
					music.setNotes(freq.getSounds());
					music.setChords(chordInterface.getChords(music.getNotes(), music.getBpb(), music.getBpm()));
					samplingGraph.repaint();
					// end of pitch value detection
					samplingGraph.initSamplingGraph();
				} catch (Exception e1) {
					e1.printStackTrace();
				}
				recalcB.setEnabled(true);
				playB.setEnabled(true);
				captB.setText(Messages.getString("FelLe.Button.Record")); //$NON-NLS-1$
			}
			// recalculating the pitch values, the chords, etc...
		} else if (obj.equals(recalcB)) {
			music.setBpb(Integer.parseInt(bpbTF.getText()));
			music.setBpm(Integer.parseInt(bpmTF.getText()));
			try {
				 freq = new Frequency(music);
				music.setNotes(freq.getSounds());
				music.setChords(chordInterface.getChords(music.getNotes(), music.getBpb(), music.getBpm()));

				samplingGraph.repaint();
				samplingGraph.initSamplingGraph();
			} catch (Exception e1) {
				System.err.println("Errot while trying to refresh the chords, must probably there is nothing recorded  yet....");//$NON-NLS-1$
				e1.printStackTrace();
			}
			// listeners for the menu
		} else {
			JMenuItem source = (JMenuItem) (e.getSource());
			// Save
			if (source.getText() == Messages.getString("FelLe.Menu.Save")) { //$NON-NLS-1$
				File file = new File(System.getProperty("user.dir")); //$NON-NLS-1$
				JFileChooser fc = new JFileChooser(file);
				fc.setFileFilter(new javax.swing.filechooser.FileFilter() {
					public boolean accept(File f) {
						if (f.isDirectory()) {
							return true;
						}
						String name = f.getName();
						if (name.endsWith(".wav")) { //$NON-NLS-1$
							return true;
						}
						return false;
					}

					public String getDescription() {
						return ".wav"; //$NON-NLS-1$
					}
				});
				if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
					if (music.getAudioBytes() == null) {
						System.out.println(Messages.getString("FelLe.NothingToSave")); //$NON-NLS-1$
					} else {
						AudioInputStream audioInputStream;
						try {
							ByteArrayInputStream bais = new ByteArrayInputStream(music.getAudioBytes());
							audioInputStream = new AudioInputStream(bais, music.getFormat(), music.getAudioBytes().length / music.getFormat().getFrameSize());
							if (AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, fc.getSelectedFile()) == -1) {
								throw new IOException(Messages.getString("FelLe.Error.IO")); //$NON-NLS-1$
							}
							audioInputStream.close();
						} catch (Exception e1) {
							System.out.println(Messages.getString("FelLe.Error.IO2") + e1.toString()); //$NON-NLS-1$
						}
					}
				} else {
					System.out.println(Messages.getString("FelLe.Canceled")); //$NON-NLS-1$
				}
			}
			// Load
			if (source.getText() == Messages.getString("FelLe.Menu.Load")) { //$NON-NLS-1$
				music.setBpb(Integer.parseInt(bpbTF.getText()));
				music.setBpm(Integer.parseInt(bpmTF.getText()));
				try {
					File file = new File(System.getProperty("user.dir")); //$NON-NLS-1$
					JFileChooser fc = new JFileChooser(file);
					fc.setFileFilter(new javax.swing.filechooser.FileFilter() {
						public boolean accept(File f) {
							if (f.isDirectory()) {
								return true;
							}
							String name = f.getName();
							if (name.endsWith(".wav")) { //$NON-NLS-1$
								return true;
							}
							return false;
						}

						public String getDescription() {
							return ".wav"; //$NON-NLS-1$
						}
					});
					if (fc.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
						createAudioBytes(fc.getSelectedFile());
					}
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
			// Exit
			if (source.getText() == Messages.getString("FelLe.Menu.Exit")) { //$NON-NLS-1$
				System.exit(0);
			}
			// About
			if (source.getText() == Messages.getString("FelLe.Menu.About")) { //$NON-NLS-1$
				JDialog f = new AboutDialog(new JFrame());
				f.setVisible(true);
			}
		}
	}

	/**
	 * Creating the audioImputStream from a file
	 * 
	 * @param file
	 *            a wav file (preferable: pcm signed, sr: 44100Hz, ss=8, 1
	 *            channel, mono, bigEndian )
	 */
	public void createAudioBytes(File file) throws Exception {
		if (file != null && file.isFile()) {
			try {
				AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);

				if (audioInputStream.getFormat().getFrameSize() == 2) {
					// not preferable wav file (the format in which audacity
					// exports the wav)
					AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 1, 2, 44100, true);
					audioInputStream = AudioSystem.getAudioInputStream(format, audioInputStream);
					double frameOriginal = audioInputStream.getFrameLength() * audioInputStream.getFormat().getFrameSize();
					byte audioBytes[] = new byte[(int) frameOriginal];
					byte audioBytesNew[] = new byte[(int) frameOriginal / 2];
					int ind = 0;
					audioInputStream.read(audioBytes);
					for (int i = 0; i < (int) frameOriginal; i += 2) {
						audioBytesNew[ind++] = (byte) ((audioBytes[i] << 8) + (audioBytes[i + 1] & 0x00ff) / 256);
					}
					music.setAudioBytes(audioBytesNew);
					audioInputStream.close();
				} else {
					// audio recorded by this program, or in the same format
					audioInputStream = AudioSystem.getAudioInputStream(music.getFormat(), audioInputStream);
					double frame = audioInputStream.getFrameLength() * audioInputStream.getFormat().getFrameSize();
					byte audioBytes[] = new byte[(int) frame];
					audioInputStream.read(audioBytes);
					music.setAudioBytes(audioBytes);
				}// end of loading file

				// calculating the pitch value and the chord for the newly
				// loaded audio
				freq = new Frequency(music);
				music.setNotes(freq.getSounds());
				music.setChords(chordInterface.getChords(music.getNotes(), music.getBpb(), music.getBpm()));
				// end of pitch detection.....

				// redrawing the wave form
				playB.setEnabled(true);
				samplingGraph.initSamplingGraph();
				samplingGraph.repaint();
				long milliseconds = (long) ((audioInputStream.getFrameLength() * 1000.0) / audioInputStream.getFormat().getFrameRate());
				duration = (milliseconds / 1000.0);
				infoLabel.setText(Messages.getString("FelLe.RecLen") + duration); //$NON-NLS-1$
				audioInputStream.close();
			} catch (Exception ex) {
				JOptionPane.showMessageDialog(this, Messages.getString("FelLe.Unsuported"), Messages.getString("FelLe.Error"), JOptionPane.ERROR_MESSAGE);
			}
		} else {
			throw new Exception(Messages.getString("FelLe.Error.NoAudio")); //$NON-NLS-1$
		}
	}

	/**
	 * main the main method of the program
	 * 
	 * @param args
	 *            should be null but its irrelevant
	 */
	public static void main(String args[]) throws Exception {
		MainFrame main = new MainFrame();
		JFrame f = new JFrame("ChordFactory"); //$NON-NLS-1$
		f.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});

		f.getContentPane().add("Center", main); //$NON-NLS-1$
		f.pack();

		f.setSize(Toolkit.getDefaultToolkit().getScreenSize().width, 400);
		f.setLocationRelativeTo(null);
		f.setVisible(true);
	}

}
